/* Copyright (C) 2011 The University of Michigan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Please send inquiries to powertutor@umich.edu */ package vn.cybersoft.obs.andriod.batterystats2.service; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; import java.util.Vector; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import vn.cybersoft.obs.andriod.batterystats2.components.OLED; import vn.cybersoft.obs.andriod.batterystats2.components.PowerComponent; import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneConstants; import vn.cybersoft.obs.andriod.batterystats2.phone.PhoneSelector; import vn.cybersoft.obs.andriod.batterystats2.phone.PowerFunction; import vn.cybersoft.obs.andriod.batterystats2.util.Counter; import vn.cybersoft.obs.andriod.batterystats2.util.HistoryBuffer; import vn.cybersoft.obs.andriod.batterystats2.util.NotificationService; import vn.cybersoft.obs.andriod.batterystats2.util.SystemInfo; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.util.SparseArray; /** This class is responsible for starting the individual power component * loggers (CPU, GPS, etc...) and collecting the information they generate. * This information is used both to write a log file that will be send back * to spidermoneky (or looked at by the user) and to implement the * ICounterService IPC interface. */ public class PowerEstimator implements Runnable { private static final String TAG = "PowerEstimator"; /* A dictionary used to assist in compression of the log files. Strings that * appear more frequently should be put towards the end of the dictionary. It * is not critical that every string that be written to the log appear here. */ private static final String DEFLATE_DICTIONARY = "onoffidleoff-hookringinglowairplane-modebatteryedgeGPRS3Gunknown" + "in-serviceemergency-onlyout-of-servicepower-offdisconnectedconnecting" + "associateconnectedsuspendedphone-callservicenetworkbegin.0123456789" + "GPSAudioWifi3GLCDCPU-power "; public static final int ALL_COMPONENTS = -1; public static final int ITERATION_INTERVAL = 1000; // 1 second private UMLoggerService context; private SharedPreferences prefs; private boolean plugged; private Vector<PowerComponent> powerComponents; private Vector<PowerFunction> powerFunctions; private Vector<HistoryBuffer> histories; private Map<Integer, String> uidAppIds; // Miscellaneous data. private HistoryBuffer oledScoreHistory; private Object fileWriteLock = new Object(); private LogUploader logUploader; private OutputStreamWriter logStream; private DeflaterOutputStream deflateStream; private Object iterationLock = new Object(); private long lastWrittenIteration; public PowerEstimator(UMLoggerService context){ this.context = context; prefs = PreferenceManager.getDefaultSharedPreferences(context); powerComponents = new Vector<PowerComponent>(); powerFunctions = new Vector<PowerFunction>(); uidAppIds = new HashMap<Integer, String>(); PhoneSelector.generateComponents(context, powerComponents, powerFunctions); histories = new Vector<HistoryBuffer>(); for(int i = 0; i < powerComponents.size(); i++) { histories.add(new HistoryBuffer(300)); } oledScoreHistory = new HistoryBuffer(0); logUploader = new LogUploader(context); openLog(true); } private void openLog(boolean init) { /* Open up the log file if possible. */ try { String logFilename = context.getFileStreamPath( "PowerTrace.log").getAbsolutePath(); if(init && prefs.getBoolean("sendPermission", true) && new File(logFilename).length() > 0) { /* There is data to send. Make sure that gets going in the sending * process before we write over any old logs. */ logUploader.upload(logFilename); } Deflater deflater = new Deflater(); deflater.setDictionary(DEFLATE_DICTIONARY.getBytes()); deflateStream = new DeflaterOutputStream( new FileOutputStream(logFilename)); logStream = new OutputStreamWriter(deflateStream); } catch(IOException e) { logStream = null; Log.e(TAG, "Failed to open log file. No log will be kept."); } } /** This is the loop that keeps updating the power profile */ public void run() { SystemInfo sysInfo = SystemInfo.getInstance(); PackageManager pm = context.getPackageManager(); BatteryStats bst = BatteryStats.getInstance(); int components = powerComponents.size(); long beginTime = SystemClock.elapsedRealtime(); for(int i = 0; i < components; i++) { powerComponents.get(i).init(beginTime, ITERATION_INTERVAL); powerComponents.get(i).start(); } IterationData[] dataTemp = new IterationData[components]; PhoneConstants phoneConstants = PhoneSelector.getConstants(context); long[] memInfo = new long[4]; int oledId = -1; for(int i = 0; i < components; i++) { if("OLED".equals(powerComponents.get(i).getComponentName())) { oledId = i; break; } } double lastCurrent = -1; /* Indefinitely collect data on each of the power components. */ boolean firstLogIteration = true; for(long iter = -1; !Thread.interrupted(); ) { long curTime = SystemClock.elapsedRealtime(); /* Compute the next iteration that we can make the ending of. We wait for the end of the iteration so that the components had a chance to collect data already. */ iter = (long)Math.max(iter + 1, (curTime - beginTime) / ITERATION_INTERVAL); /* Sleep until the next iteration completes. */ try { Thread.currentThread().sleep( beginTime + (iter + 1) * ITERATION_INTERVAL - curTime); } catch(InterruptedException e) { break; } int totalPower = 0; for(int i = 0; i < components; i++) { PowerComponent comp = powerComponents.get(i); IterationData data = comp.getData(iter); dataTemp[i] = data; if(data == null) { /* No data present for this timestamp. No power charged. */ continue; } SparseArray<PowerData> uidPower = data.getUidPowerData(); for(int j = 0; j < uidPower.size(); j++) { int uid = uidPower.keyAt(j); PowerData powerData = uidPower.valueAt(j); int power = (int)powerFunctions.get(i).calculate(powerData); powerData.setCachedPower(power); histories.get(i).add(uid, iter, power); if(uid == SystemInfo.AID_ALL) { totalPower += power; } if(i == oledId) { OLED.OledData oledData = (OLED.OledData)powerData; if(oledData.pixPower >= 0) { oledScoreHistory.add(uid, iter, (int)(1000 * oledData.pixPower)); } } } } /* Update the uid set. */ synchronized(fileWriteLock) { synchronized(uidAppIds) { for(int i = 0; i < components; i++) { IterationData data = dataTemp[i]; if(data == null) { continue; } SparseArray<PowerData> uidPower = data.getUidPowerData(); for(int j = 0; j < uidPower.size(); j++) { int uid = uidPower.keyAt(j); if(uid < SystemInfo.AID_APP) { uidAppIds.put(uid, null); } else { /* We only want to update app names when logging so the associcate * message gets written. */ String appId = uidAppIds.get(uid); String newAppId = sysInfo.getAppId(uid, pm); if(!firstLogIteration && logStream != null && (appId == null || !appId.equals(newAppId))) { try { logStream.write("associate " + uid + " " + newAppId + "\n"); } catch(IOException e) { Log.w(TAG, "Failed to write to log file"); } } uidAppIds.put(uid, newAppId); } } } }} synchronized(iterationLock) { lastWrittenIteration = iter; } /* Update the icon display every 15 iterations. */ if(iter % 15 == 14) { final double POLY_WEIGHT = 0.02; int count = 0; int[] history = getComponentHistory(5 * 60, -1, SystemInfo.AID_ALL, -1); double weightedAvgPower = 0; for(int i = history.length - 1; i >= 0; i--) { if(history[i] != 0) { count++; weightedAvgPower *= 1.0 - POLY_WEIGHT; weightedAvgPower += POLY_WEIGHT * history[i] / 1000.0; } } double avgPower = -1; if(count != 0) { avgPower = weightedAvgPower / (1.0 - Math.pow(1.0 - POLY_WEIGHT, count)); } avgPower *= 1000; /*context.updateNotification((int)Math.min(8, 1 + 8 * avgPower / phoneConstants.maxPower()), avgPower);*/ } /* Update the widget. */ if(iter % 60 == 0) { //PowerWidget.updateWidget(context, this); } if(bst.hasCurrent()) { double current = bst.getCurrent(); if(current != lastCurrent) { writeToLog("batt_current " + current + "\n"); lastCurrent = current; } } if(iter % (5*60) == 0) { if(bst.hasTemp()) { writeToLog("batt_temp " + bst.getTemp() + "\n"); } if(bst.hasCharge()) { writeToLog("batt_charge " + bst.getCharge() + "\n"); } } if(iter % (30*60) == 0) { if(Settings.System.getInt(context.getContentResolver(), "screen_brightness_mode", 0) != 0) { writeToLog("setting_brightness automatic\n"); } else { int brightness = Settings.System.getInt( context.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, -1); if(brightness != -1) { writeToLog("setting_brightness " + brightness + "\n"); } } int timeout = Settings.System.getInt( context.getContentResolver(), Settings.System.SCREEN_OFF_TIMEOUT, -1); if(timeout != -1) { writeToLog("setting_screen_timeout " + timeout + "\n"); } String httpProxy = Settings.Secure.getString( context.getContentResolver(), Settings.Secure.HTTP_PROXY); if(httpProxy != null) { writeToLog("setting_httpproxy " + httpProxy + "\n"); } } /* Let's only grab memory information every 10 seconds to try to keep log * file size down and the notice_data table size down. */ boolean hasMem = false; if(iter % 10 == 0) { hasMem = sysInfo.getMemInfo(memInfo); } synchronized(fileWriteLock) { if(logStream != null) try { if(firstLogIteration) { firstLogIteration = false; logStream.write("time " + System.currentTimeMillis() + "\n"); Calendar cal = new GregorianCalendar(); logStream.write("localtime_offset " + (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) + "\n"); logStream.write("model " + phoneConstants.modelName() + "\n"); if(NotificationService.available()) { logStream.write("notifications-active\n"); } if(bst.hasFullCapacity()) { logStream.write("batt_full_capacity " + bst.getFullCapacity() + "\n"); } synchronized(uidAppIds) { for(int uid : uidAppIds.keySet()) { if(uid < SystemInfo.AID_APP) { continue; } logStream.write("associate " + uid + " " + uidAppIds.get(uid) + "\n"); } } } logStream.write("begin " + iter + "\n"); logStream.write("total-power " + (long)Math.round(totalPower) + '\n'); if(hasMem) { logStream.write("meminfo " + memInfo[0] + " " + memInfo[1] + " " + memInfo[2] + " " + memInfo[3] + "\n"); } for(int i = 0; i < components; i++) { IterationData data = dataTemp[i]; if(data != null) { String name = powerComponents.get(i).getComponentName(); SparseArray<PowerData> uidData = data.getUidPowerData(); for(int j = 0; j < uidData.size(); j++) { int uid = uidData.keyAt(j); PowerData powerData = uidData.valueAt(j); if(uid == SystemInfo.AID_ALL) { logStream.write(name + " " + (long)Math.round( powerData.getCachedPower()) + "\n"); powerData.writeLogDataInfo(logStream); } else { logStream.write(name + "-" + uid + " " + (long)Math.round( powerData.getCachedPower()) + "\n"); } } data.recycle(); } } } catch(IOException e) { Log.w(TAG, "Failed to write to log file"); } if(iter % 15 == 0 && prefs.getBoolean("sendPermission", true)) { /* Allow for LogUploader to decide if the log needs to be uploaded and * begin uploading if it decides it's necessary. */ if(logUploader.shouldUpload()) { try { logStream.close(); } catch(IOException e) { Log.w(TAG, "Failed to flush and close log stream"); } logStream = null; logUploader.upload(context.getFileStreamPath( "PowerTrace.log").getAbsolutePath()); openLog(false); firstLogIteration = true; } } } } /* Blank the widget's display and turn off power button. */ //PowerWidget.updateWidgetDone(context); /* Have all of the power component threads exit. */ logUploader.interrupt(); for(int i = 0; i < components; i++) { powerComponents.get(i).interrupt(); } try { logUploader.join(); } catch(InterruptedException e) { } for(int i = 0; i < components; i++) { try { powerComponents.get(i).join(); } catch(InterruptedException e) { } } /* Close the logstream so that everything gets flushed and written to file * before we have to quit. */ synchronized(fileWriteLock) { if(logStream != null) try { logStream.close(); } catch(IOException e) { Log.w(TAG, "Failed to flush log file on exit"); } } } public void plug(boolean plugged) { logUploader.plug(plugged); } public void writeToLog(String m) { synchronized(fileWriteLock) { if(logStream != null) try { logStream.write(m); } catch(IOException e) { Log.w(TAG, "Failed to write message to power log"); } } } public String[] getComponents() { int components = powerComponents.size(); String[] ret = new String[components]; for(int i = 0; i < components; i++) { ret[i] = powerComponents.get(i).getComponentName(); } return ret; } public int[] getComponentsMaxPower() { PhoneConstants constants = PhoneSelector.getConstants(context); int components = powerComponents.size(); int[] ret = new int[components]; for(int i = 0; i < components; i++) { ret[i] = (int)constants.getMaxPower( powerComponents.get(i).getComponentName()); } return ret; } public int getNoUidMask() { int components = powerComponents.size(); int ret = 0; for(int i = 0; i < components; i++) { if(!powerComponents.get(i).hasUidInformation()) { ret |= 1 << i; } } return ret; } public int[] getComponentHistory(int count, int componentId, int uid, long iteration) { if(iteration == -1) synchronized(iterationLock) { iteration = lastWrittenIteration; } int components = powerComponents.size(); if(componentId == ALL_COMPONENTS) { int[] result = new int[count]; for(int i = 0; i < components; i++) { int[] comp = histories.get(i).get(uid, iteration, count); for(int j = 0; j < count; j++) { result[j] += comp[j]; } } return result; } if(componentId < 0 || components <= componentId) return null; return histories.get(componentId).get(uid, iteration, count); } public long[] getTotals(int uid, int windowType) { int components = powerComponents.size(); long[] ret = new long[components]; for(int i = 0; i < components; i++) { ret[i] = histories.get(i).getTotal(uid, windowType) * ITERATION_INTERVAL / 1000; } return ret; } public long getRuntime(int uid, int windowType) { long runningTime = 0; int components = powerComponents.size(); for(int i = 0; i < components; i++) { long entries = histories.get(i).getCount(uid, windowType); runningTime = entries > runningTime ? entries : runningTime; } return runningTime * ITERATION_INTERVAL / 1000; } public long[] getMeans(int uid, int windowType) { long[] ret = getTotals(uid, windowType); long runningTime = getRuntime(uid, windowType); runningTime = runningTime == 0 ? 1 : runningTime; for(int i = 0; i < ret.length; i++) { ret[i] /= runningTime; } return ret; } public UidInfo[] getUidInfo(int windowType, int ignoreMask) { long iteration; synchronized(iterationLock) { iteration = lastWrittenIteration; } int components = powerComponents.size(); synchronized(uidAppIds) { int pos = 0; UidInfo[] result = new UidInfo[uidAppIds.size()]; for(Integer uid : uidAppIds.keySet()) { UidInfo info = UidInfo.obtain(); int currentPower = 0; for(int i = 0; i < components; i++) { if((ignoreMask & 1 << i) == 0) { currentPower += histories.get(i).get(uid, iteration, 1)[0]; } } double scale = ITERATION_INTERVAL / 1000.0; info.init(uid, currentPower, sumArray(getTotals(uid, windowType), ignoreMask) * ITERATION_INTERVAL / 1000, getRuntime(uid, windowType) * ITERATION_INTERVAL / 1000); result[pos++] = info; } return result; } } private long sumArray(long[] A, int ignoreMask) { long ret = 0; for(int i = 0; i < A.length; i++) { if((ignoreMask & 1 << i) == 0) { ret += A[i]; } } return ret; } public long getUidExtra(String name, int uid) { if("OLEDSCORE".equals(name)) { long entries = oledScoreHistory.getCount(uid, Counter.WINDOW_TOTAL); if(entries <= 0) return -2; double result = oledScoreHistory.getTotal(uid, Counter.WINDOW_TOTAL) / 1000.0; result /= entries; PhoneConstants phoneConstants = PhoneSelector.getConstants(context); result *= 255 / (phoneConstants.getMaxPower("OLED") - phoneConstants.oledBasePower()); return (long)Math.round(result * 100); } return -1; } }